<template>
  <div
    ref="virtualTree"
    class="virtual-tree padding-left--small padding-right--small"
    :style="{ width, height }"
    @scroll="refreshList"
  >
    <!-- v-loading不能放置在 virtualTree顶层div上, 否则会导致 搜索后鼠标无法滚动的bug-->
    <div
      v-if="isLoading || outLoading"
      v-loading="isLoading || outLoading"
      class="loading-mask"
    />
    <div
      class="virtual-tree__placeholder"
      v-if="!virtualList.length"
    >
      {{ emptyText }}
    </div>
    <div :style="{height: `${listHeight}px`, 'padding-top': `${listPaddingTop}px`}">
      <virtual-tree-node
        v-for="node in virtualList"
        ref="treeNodes"
        :key="node.key"
        :node="node"
        :render-content="renderContent"
        :show-checkbox="showCheckbox"
        :show-folder-icon="!list && showFolderIcon"
        :folder-expand-icon="folderExpandIcon"
        :folder-no-expand-icon="folderNoExpandIcon"
        :draggable="draggable"
        :show-tooltip="showTooltip"
        @clickNode="clickNode"
        @checkNode="checkNode"
        @node-dragstart="handleDragStart"
      />
    </div>
  </div>
</template>
<script lang="ts">
import { Vue, Component, Prop, Watch } from 'vue-property-decorator';
import TreeNode from './node';
import virtualTreeNode from './treeNode.vue';
import _ from 'lodash';

interface TreeOption {
    key: string,
    label: string,
    children: string,
    disabled: string|Function,
    check: string
}

@Component({
  components: {
    virtualTreeNode,
  },
})
export default class VirtualTree extends Vue {
    @Prop({ type: Array, required: true }) data: []
    @Prop({ type: Object }) option: TreeOption
    @Prop({ type: Boolean, default: false }) lazy: boolean // 子节点懒加载，要配合load使用
    @Prop({ type: Boolean, default: false }) showCheckbox: boolean
    @Prop({ type: Boolean, default: false }) checkStrictly: boolean //  在显示复选框的情况下，是否严格的遵循父子不互相关联的做法
    @Prop({ type: Boolean, default: true }) expandOnClickNode: boolean // 点击节点展开或折叠，为 false 时，点击展开按钮才会展开/折叠
    @Prop({ type: Boolean, default: false }) list: boolean
    @Prop({ type: [String, Number] }) currentNodeKey: string|number
    @Prop({ type: Array, default: () => [] }) defaultCheckedKeys: [string|number]
    @Prop({ type: Array, default: () => [] }) defaultCheckedNodeKeys: [string|number]
    @Prop({ type: Array, default: () => [] }) defaultIndeterminateKeys: [string|number]
    @Prop({ type: String, default: '暂无数据' }) emptyText: string
    @Prop({ type: Function }) load: Function // 子节点展开时的调用
    @Prop({ type: Function }) filterNodeMethod: Function
    @Prop({ type: Function }) renderContent: Function
    @Prop({ type: String, default: '' }) width: string
    @Prop({ type: String, default: '' }) height: string
    @Prop({ type: String, default: null }) folderExpandIcon: string // 树节点的展开icon，iconfont样式名
    @Prop({ type: String, default: null }) folderNoExpandIcon: string // 树节点未展开时icon
    @Prop({ type: Boolean, default: true }) showFolderIcon: boolean // 是否显示节点icon
    @Prop({ type: Boolean, default: true }) showCurrentNode: boolean // 是否高亮当前节点
    @Prop({ type: [String, Function] }) nodeKey: string|Function // 用户自定义以什么字段作为node.key的值, 或者通过方法生成key
    @Prop({ type: Boolean, default: false }) keepExpandStatus: boolean // 是否保持节点展开状态
    @Prop({ type: Boolean, default: false }) keepFilterStatus: boolean // 是否保持检索状态
    @Prop({ type: String, default: '' }) filterKey: string // 检索关键字，配合keepFilterStatus使用
    @Prop({ type: Function }) draggable: Function // 检查节点是否能被拖拽
    @Prop({ type: Boolean, default: true }) showTooltip: boolean // 鼠标hover节点展示title
    @Prop({ type: Boolean, default: false }) outLoading: boolean // 外部控制loading显示
    @Prop({ type: Number }) pageSize: number // 分页大小
    @Prop({ type: Boolean, default: true }) alwaysTop: boolean // 未选中节点时是否每次数据更新都回到数的根部

    // data
    nodes: TreeNode[] = []
    startIndex: number = 0
    endIndex: number = 0
    _nodes: TreeNode[] = [] // 搜索模式下数据备份
    _startIndex: number = 0
    _endIndex: number = 0
    itemHeight: number = 0
    visionHeight: number = 0
    filterTimer: any = null
    initHeight: Function = () => {}
    isLoading: boolean = false
    expandedKeys: any[] = []

    // computed
    get virtualList () {
      return this.visibleList.slice(this.startIndex, this.endIndex + 1);
    }

    get visibleList () {
      return this.nodes.filter(node => node.visible);
    }

    get defaultOpt () {
      return Object.assign({
        key: 'key',
        label: 'label',
        children: 'children',
        disabled: 'disabled',
        isLeaf: 'isLeaf',
      }, this.option);
    }

    get listHeight () {
      return this.itemHeight * this.visibleList.length;
    }

    get listPaddingTop () {
      return this.itemHeight * this.startIndex;
    }

    get visionMax () {
      return this.itemHeight === 0 ? 0 : Math.ceil(this.visionHeight / this.itemHeight);
    }

    get nodeMap () {
      const map = new Map();
      this.nodes.forEach(node => {
        map.set(node.key, node);
      });

      return map;
    }

    // methods
    traverse (data: [Object], parentNode: TreeNode|null, fn: Function, level: number = 0) {
      const childrenProp = this.defaultOpt.children;
      data.forEach(item => {
        const node = fn.call(this, item, parentNode, level);
        if (Array.isArray(item[childrenProp])) {
          this.traverse(item[childrenProp], node, fn, level + 1);
        }
      });
      // 修正父级的选中状态
      if (this.showCheckbox && parentNode?.isIndeterminate && parentNode?.children?.length) {
        if (parentNode.children.every(item => item.isCheck)) {
          parentNode.isIndeterminate = false;
          parentNode.isCheck = true;
        }
      }
    }

    initNodeData (data: [Object], parentNode?: TreeNode, isLazyAppend?: boolean) {
      const { key, label, children, disabled, check, isLeaf } = this.defaultOpt;
      const disabledFunc: Function = typeof disabled === 'function' ? disabled : item => item[disabled];
      const nodeList = [];
      let nodeKeyFunc = (item, parent) => item.isLoadMore ? item[key] : item[key] + '' + this._recursionKey(parent);
      if (typeof this.nodeKey === 'function') {
        nodeKeyFunc = (item, parent, level) => this.nodeKey(item, parent, level);
      } else if (this.nodeKey) {
        nodeKeyFunc = item => item[this.nodeKey];
      }

      let currentNode;
      const parentLevel = (parentNode?.level || 0) + (isLazyAppend ? 1 : 0);
      const fn = (item, parent, level) => {
        const nodeKey = nodeKeyFunc(item, parent, level);

        // 根据记录恢复节点展开状态
        let expanded = false;
        if (this.keepExpandStatus) {
          expanded = this.expandedKeys.indexOf(nodeKey) !== -1;
        }

        const option = {
          label: item[label],
          key: nodeKey,
          isFolder: Array.isArray(item[children]) || (!_.isUndefined(item[isLeaf]) && !item[isLeaf]),
          isCurrent: item[key] === this.currentNodeKey && this.showCurrentNode,
          isCheck: this.showCheckbox && (item[check] || (parent?.isCheck && !this.checkStrictly) || this.defaultCheckedKeys.includes(item[key]) || this.defaultCheckedNodeKeys.includes(nodeKey)),
          disabled: disabledFunc(item),
          data: item,
          parent,
          level,
          expanded,
          // pagination
          isLoadMore: item.isLoadMore ?? false,
          pageIndex: item.pageIndex ?? 0,
          pageSize: this.pageSize,
          total: item.total ?? 0,
          // loaded: item.loaded ?? false,
          loaded: item.loaded ?? (Boolean(item.children?.length) || false),
        };
        const node = new TreeNode(option);
        nodeList.push(node);

        if (node.isCurrent) {
          currentNode = node;
        }

        if (this.defaultIndeterminateKeys.includes(node.key)) {
          node.isIndeterminate = true;
        }

        if (node.isCheck && parent && !parent.isCheck) {
          node.traverseParent(n => { n.isIndeterminate = true; });
        }

        if (parent) {
          parent.children.push(node);
        }

        return node;
      };

      this.traverse(data, parentNode, fn, parentLevel);
      currentNode && currentNode.expandRoot();
      return nodeList;
    }

    initNodes (data: [], parentNode?: TreeNode) {
      // 清楚检索定时器
      if (this.filterTimer) {
        clearTimeout(this.filterTimer);
        this.isLoading = false;
      }
      this.nodes = this.initNodeData(data, parentNode);
      if (this.keepFilterStatus && this.filterKey !== '') {
        const keywords = this.filterKey;
        this.doFilter(keywords);
      }
      this.startIndex = 0;
      this.endIndex = this.nodes.length ? 1 : 0;
    }

    _initHeight () {
      this.$nextTick(() => {
        if (!this.$refs.treeNodes) {
          return;
        }

        const firstNode = this.$refs.treeNodes[0];
        if (!firstNode || firstNode.$el.offsetHeight === 0) {
          return;
        }

        this.itemHeight = firstNode.$el.offsetHeight;
        this.visionHeight = this.$el.offsetHeight;
        this.setEndIndexByDefault();

        const curNode = this._find(this.currentNodeKey).node;
        if (this.currentNodeKey && curNode && curNode.visible) {
          this.scrollTo(curNode.key);
        } else if (this.alwaysTop) {
          this.scrollTo(this.visibleList[this.startIndex].key);
        }
      });
    }

    _find (key) {
      let index = -1;
      const node = this.nodes.find((n, idx) => {
        if (n.key === key) {
          index = idx;
          return n;
        }
        // eslint-disable-next-line eqeqeq
        if (n.data?.[this.defaultOpt.key] == key) {
          index = idx;
          return true;
        }
        return false;
      });

      return { node, index };
    }

    filter (keywords: string) {
      clearTimeout(this.filterTimer);
      this.isLoading = true;
      this.filterTimer = setTimeout(() => {
        this.doFilter(keywords);
        let startNode = null;
        if (!keywords && keywords !== '') {
          startNode = this.nodes.find((node) => node.isCurrent);
        } else {
          startNode = this.visibleList[0];
        }

        if (startNode) {
          this.scrollTo(startNode.key);
        } else {
          // 当过滤后startNode为空时说明没有符合关键字的值，这个时候也应该重置startIndex为0
          this.startIndex = 0;
        }
        this.isLoading = false;
        this.$emit('searchSuccess');
      }, 300);
    }

    doFilter (keywords: string) {
      if (!keywords && keywords !== '') {
        this.nodes.forEach((node) => {
          node.visible = !!(!node.parent || node.parent.expanded);
          node.isFilter = true;
        });
      } else {
        // 兼容 label 是数字 0 的场景
        const filterMethod = typeof this.filterNodeMethod === 'function'
          ? this.filterNodeMethod
          : (value, data, node) => node.label !== undefined && (node.label + '').toLocaleLowerCase().includes(value);
        this.nodes.forEach(node => {
          node.visible = filterMethod(keywords.toLocaleLowerCase(), node.data, node);
          node.isFilter = node.visible;
          if (node.visible) {
            node.traverseParent(parent => {
              parent.visible = true;
              parent.expanded = true;
              node.isFilter = true;
            });
          }
        });
      }
    }

    setCurrentNode (key: string) {
      // 递归展开父节点
      const fn = (node) => {
        if (node.parent) {
          node.parent.expanded = true;
          node.parent.children.forEach(child => {
            child.visible = true;
          });
          fn(node.parent);
        }
      };
      this.nodes.some(node => {
        // 若仪表盘在文件夹中，传入的 key 是仪表盘id，而 node 的 key 是 ${仪表盘id}__${文件夹id}
        if (node.key === key || (node.key?.startsWith && node.key?.startsWith(key))) {
          this.clearCurrentNode();
          node.isCurrent = true;
          fn(node);
          return true;
        }
        return false;
      });
    }

    clearCurrentNode () {
      const prev = this.nodes.find(node => node.isCurrent);
      prev && prev.toggleCurrent();
    }

    setCheckedNodes (keys, val = true, updateChild = false) {
      if (!Array.isArray(keys)) {
        return;
      }

      keys.forEach(key => {
        this.setChecked(key, val, updateChild);
      });
    }

    setChecked (key, val, updateChild) {
      const { key: nodeKey } = this.defaultOpt;
      const node = this.nodeMap.get(key) || this.nodes.find(item => item.data[nodeKey] === key);
      if (node) {
        node.isCheck = val;
        node.isIndeterminate = false;
        if (!this.checkStrictly) {
          this.updateParentCheck(node);
        }
        if (updateChild && !this.checkStrictly) {
          // 取消选中，需要同步更新子级的选中状态
          node.traverse(n => {
            if (n === node) return;
            n.isCheck = val;
            n.isIndeterminate = false;
          });
        }
      }
    }

    setIndeterminateNodes (keys) {
      if (!Array.isArray(keys)) {
        return;
      }

      keys.forEach(key => {
        this.setIndeterminate(key);
      });
    }

    setIndeterminate (key) {
      const { key: nodeKey } = this.defaultOpt;
      const node = this.nodeMap.get(key) || this.nodes.find(item => item.data[nodeKey] === key);
      if (node && !node.isCheck) {
        node.isIndeterminate = true;
      }
    }

    setCheckedAll (val = true) {
      this.nodes.forEach(item => {
        item.isCheck = val;
        item.isIndeterminate = false;
      });
    }

    // 选中全部可见节点，与setCheckedAll区别是：它只把过滤好的节点进行操作（如搜索过滤）
    setVisibleListCheckedAll (val) {
      this.visibleList.forEach(node => {
        node.traverse(item => {
          item.isCheck = val;
          item.isIndeterminate = false;
        });
      });
    }

    // 可见的列表节点是否全部选中
    isVisibleListCheckedAll () {
      return this.visibleList.every(node => {
        let check = true;
        node.traverse(item => {
          if (!item.isCheck) {
            check = false;
            return true;
          }
        });
        return check;
      });
    }

    getCheckedNodes () {
      return this.nodes.filter(node => node.isCheck);
    }

    getIndeterminateNodes () {
      return this.nodes.filter(node => node.isIndeterminate && !node.isCheck);
    }

    setEndIndexByDefault () {
      this.endIndex = Math.min(this.startIndex + this.visionMax, this.visibleList.length - 1);
    }

    refreshList (ev?: Event) {
      // 修复v-show场景第二次打开时空白或内容只有一个，主要出现在dialog
      this.itemHeight === 0 && this._initHeight();
      if (ev) {
        this.startIndex = Math.floor(ev.target.scrollTop / this.itemHeight);
      } else {
        this.startIndex = 0;
      }
      this.setEndIndexByDefault();
      if (ev.target) {
        const listEl = ev.target;
        const scrollTop = listEl.scrollTop;
        const windowHeight = listEl.clientHeight;
        const scrollHeight = listEl.scrollHeight;
        // 滚动到底部
        if (scrollHeight - scrollTop - windowHeight < 80) {
          this.$emit('scroll-end', ev);
        }
      }
    }

    // 清空展开节点数组
    setExpandedKeysEmpty () {
      this.expandedKeys = [];
    }

    clickNode (data: [Object], node: TreeNode, isExpandIconClick: boolean) {
      if (node.isFolder) {
        if (this.expandOnClickNode || (!this.expandOnClickNode && isExpandIconClick)) {
          node.toggleExpand();

          // 记录节点展开状态
          if (this.keepExpandStatus) {
            const expandIndex = this.expandedKeys.indexOf(node.key);
            if (node.expanded && expandIndex === -1) {
              this.expandedKeys.push(node.key);
            } else if (!node.expanded && expandIndex !== -1) {
              this.expandedKeys.splice(expandIndex, 1);
            }
          }
          // 懒加载开启 & 有load请求函数 & 父节点被展开 & 父节点没有加载过子节点
          if (this.lazy && this.load && node.expanded && !node.loaded) {
            this.lazyLoad(node);
          } else {
            this.setEndIndexByDefault();
          }
        }
      } else if (!node.isLoadMore) {
        this.clearCurrentNode();
        this.showCurrentNode && node.toggleCurrent();
        node.expandRoot();
      }
      this.$emit('clickNode', data, node);
    }

    checkNode (data:any, node: TreeNode) {
      node.isIndeterminate = false;
      if (!this.checkStrictly) {
        node.traverse(n => {
          if (n === node) {
            return;
          }

          n.isCheck = node.isCheck;
          n.isIndeterminate = false;
        });
        this.updateParentCheck(node);
      }

      this.$emit('checkNode', data, node);
    }

    lazyLoad (node: TreeNode) {
      node.toggleLoading();
      // 发送请求获取子节点
      return this.load(node)
        .then((data: [Object]) => {
          if (Array.isArray(data) && data.length > 0) {
            this.append(data, node.key);
            node.loaded = true;
          } else {
            node.loaded = true;
          }
          return data;
        }, e => {
          console.error(e);
        })
        .finally(() => {
          node.toggleLoading();
        });
    }

    updateParentCheck (node) {
      node.traverseParent(n => {
        if (n.children.some(child => child.isIndeterminate)) {
          n.isCheck = false;
          n.isIndeterminate = true;
        } else if (n.children.every(child => child.isCheck)) {
          n.isCheck = true;
          n.isIndeterminate = false;
        } else if (n.children.every(child => !child.isCheck)) {
          n.isCheck = false;
          n.isIndeterminate = false;
        } else {
          n.isCheck = false;
          n.isIndeterminate = true;
        }
      });
    }

    scrollTo (key: string|number) {
      this.$nextTick(() => {
        const idx = this.visibleList.findIndex(node => node.key === key);
        if (idx < 0) return;
        const page = Math.floor((idx) / this.visionMax);
        // 如果可见节点数量小于可视化区域最大数量，则直接滚动至顶部
        if (this.visibleList.length <= this.visionMax) {
          this.startIndex = 0;
          this.endIndex = this.visibleList.length - 1;
        } else if ((page + 1) * this.visionMax > this.visibleList.length) {
          // 如果节点所在页的下一页的数量超过节点数量，则按照最后一页来展示
          // eg 可见列表为50个，可视区域为20个，当前节点为41
          this.startIndex = this.visibleList.length - this.visionMax;
          this.endIndex = this.visibleList.length - 1;
        } else {
          this.startIndex = page * this.visionMax;
          this.endIndex = this.visionMax * (page + 1) - 1;
        }
        this.$el.scrollTo(0, this.startIndex * this.itemHeight);
      });
    }

    nodeBackups (revert) {
      if (revert) {
        // 还原
        this.nodes = this._nodes;
        this.startIndex = this._startIndex;
        this.endIndex = this._endIndex;
        this._nodes = [];
        this.scrollTo(this.visibleList[this.startIndex].key);
      } else {
        // 备份
        this._nodes = this.nodes;
        this._startIndex = this.startIndex;
        this._endIndex = this.endIndex;
      }
    }

    // eslint-disable-next-line no-unused-vars
    append (data: [Object], parentKey: string|number, referenceKey?: string|number) {
      const parent = this._find(parentKey);
      if (!parent.node) {
        return;
      }
      const preLength = [].concat(parent.node.children).length ?? 0;
      const nodeList: TreeNode[] = this.initNodeData(data, parent.node, true);

      const ref = this._find(parent.node.children[0]);
      let idx = ref.index;
      if (idx === -1) {
        idx = parent.index + 1;
      }
      this.nodes.splice(idx, preLength, ...nodeList);

      parent.node.children = nodeList;
      parent.node.data.children = data;

      this.setEndIndexByDefault();
    }

    _recursionKey (node: TreeNode) {
      if (!node) {
        return '';
      }
      let a: TreeNode = node;
      let key = '';
      while (a) {
        key += '__' + a.key;
        a = a.parent;
      }
      return key;
    }

    handleDragStart (obj) {
      this.$emit('node-dragstart', obj);
    }

    // watch
    @Watch('data')
    watchData (newVal) {
      this.initNodes(newVal);
      this.initHeight();
    }

    // watch
    @Watch('defaultCheckedNodeKeys')
    watchDefaultCheckedKeys (newVal) {
      if (newVal.length) {
        this.setCheckedNodes(newVal);
      }
    }

    // watch
    @Watch('defaultIndeterminateKeys')
    watchDefaultIndeterminateKeys (newVal) {
      if (newVal.length) {
        this.setIndeterminateNodes(newVal);
      }
    }

    // hooks
    created () {
      this.initNodes(this.data);
      this.initHeight = _.debounce(this._initHeight, 100);
    }

    mounted () {
      this.initHeight();
    }

    updated () {
      // 存在没正确触发导致itemHeight:0的场景，需要后续调用
      // 例如：多级用户管理dialog有三个组件切换，此时treeNode没有内容
      if (this.itemHeight === 0) {
        this.initHeight();
      }
    }
}
</script>
<style lang="scss">
    .virtual-tree {
      overflow: auto;
      height: 280px;
    }
    .loading-mask{
      width: 100%;
      height: 100%;
      float: left;
    }
    .virtual-tree__placeholder {
      display: flex;
      justify-content: center;
      align-items: center;
      width: 100%;
      height: 100%;
    }
</style>
